/**
 * Copyright (c) Huawei Technologies Co., Ltd. 2019-2021. All rights reserved.
 * Description:
 */
#include "plugins/ecmascript/runtime/base/builtins_base.h"
#include "plugins/ecmascript/runtime/internal_call_params.h"
#include "plugins/ecmascript/runtime/js_async_generator_object.h"
#include "plugins/ecmascript/runtime/js_promise.h"
#include "plugins/ecmascript/runtime/js_iterator.h"
#include "plugins/ecmascript/runtime/global_env.h"
#include "plugins/ecmascript/runtime/generator_helper.h"

namespace ark::ecmascript {
enum class AsyncGeneratorRequest : ark::ArraySizeT { COMPLETION, PROMISE_CAPABILITY };

JSTaggedValue JSAsyncGeneratorResolveNextFunction::AsyncGeneratorResolveNextFulfilled(EcmaRuntimeCallInfo *argv)
{
    JSThread *thread = argv->GetThread();
    [[maybe_unused]] EcmaHandleScope handleScope(thread);

    // 1. Let F be the active function object.
    JSHandle<JSAsyncGeneratorResolveNextFunction> thisFunc(builtins_common::GetConstructor(argv));
    JSHandle<JSAsyncGeneratorObject> asyncGenObject(thread, thisFunc->GetAsyncGenerator());

    // 2. Set F.[[Generator]].[[AsyncGeneratorState]] to completed.
    asyncGenObject->SetState(thread, JSGeneratorState::COMPLETED);

    // 3. Return ! AsyncGeneratorResolve(F.[[Generator]], value, true).
    JSHandle<JSTaggedValue> value = builtins_common::GetCallArg(argv, 0);
    return JSAsyncGeneratorObject::AsyncGeneratorResolve(thread, JSHandle<JSTaggedValue>::Cast(asyncGenObject), value,
                                                         true)
        .GetTaggedValue();
}

JSTaggedValue JSAsyncGeneratorResolveNextFunction::AsyncGeneratorResolveNextRejected(EcmaRuntimeCallInfo *argv)
{
    JSThread *thread = argv->GetThread();
    [[maybe_unused]] EcmaHandleScope handleScope(thread);

    // 1. Let F be the active function object.
    JSHandle<JSAsyncGeneratorResolveNextFunction> thisFunc(builtins_common::GetConstructor(argv));
    JSHandle<JSAsyncGeneratorObject> asyncGenObject(thread, thisFunc->GetAsyncGenerator());

    // 2. Set F.[[Generator]].[[AsyncGeneratorState]] to completed.
    asyncGenObject->SetState(thread, JSGeneratorState::COMPLETED);

    // 3. Return ! AsyncGeneratorReject(F.[[Generator]], reason).
    JSHandle<JSTaggedValue> reason = builtins_common::GetCallArg(argv, 0);
    return JSAsyncGeneratorObject::AsyncGeneratorReject(thread, JSHandle<JSTaggedValue>::Cast(asyncGenObject), reason)
        .GetTaggedValue();
}

// 27.6.3.3 AsyncGeneratorValidate(generator)
JSTaggedValue JSAsyncGeneratorObject::AsyncGeneratorValidate(JSThread *thread, const JSHandle<JSTaggedValue> &generator)
{
    if (!generator->IsAsyncGeneratorObject()) {
        THROW_TYPE_ERROR_AND_RETURN(thread, "Not an async generator object.", JSTaggedValue::Exception());
    }

    return JSTaggedValue::Hole();
}

// 27.6.3.4 AsyncGeneratorResolve (generator, value, done)
JSHandle<JSTaggedValue> JSAsyncGeneratorObject::AsyncGeneratorResolve(JSThread *thread,
                                                                      const JSHandle<JSTaggedValue> &generator,
                                                                      const JSHandle<JSTaggedValue> &value, bool done)
{
    [[maybe_unused]] EcmaHandleScope handleScope(thread);

    const GlobalEnvConstants *globalConst = thread->GlobalConstants();

    // 1. Assert: generator is an AsyncGenerator instance.
    ASSERT(generator->IsAsyncGeneratorObject());
    JSAsyncGeneratorObject *asyncGenObject = JSAsyncGeneratorObject::Cast(generator->GetHeapObject());

    // 2. Let queue be generator.[[AsyncGeneratorQueue]].
    JSHandle<TaggedQueue> queue(thread, TaggedQueue::Cast(asyncGenObject->GetAsyncGeneratorQueue().GetHeapObject()));

    // 3. Assert: queue is not an empty List.
    ASSERT(!queue->Empty());

    // 4. Let next be the first element of queue.
    // 5. Remove the first element from queue.
    JSHandle<TaggedArray> nextAsyncGeneratorRequest(thread, queue->Pop(thread));

    // 6. Let promiseCapability be next.[[Capability]].
    JSTaggedValue promiseCapabilityValue =
        nextAsyncGeneratorRequest->Get(thread, helpers::ToUnderlying(AsyncGeneratorRequest::PROMISE_CAPABILITY));
    JSHandle<PromiseCapability> promiseCapability(thread,
                                                  PromiseCapability::Cast(promiseCapabilityValue.GetHeapObject()));

    // 7. Let iteratorResult be ! CreateIterResultObject(value, done).
    JSHandle<JSObject> iteratorResult = JSIterator::CreateIterResultObject(thread, value, done);

    // 8. Perform ! Call(promiseCapability.[[Resolve]], undefined, « iteratorResult »).
    JSHandle<JSTaggedValue> thisArg = globalConst->GetHandledUndefined();

    JSHandle<JSTaggedValue> resolve(thread, promiseCapability->GetResolve());
    auto info = NewRuntimeCallInfo(thread, resolve, thisArg, JSTaggedValue::Undefined(), 1);
    info->SetCallArgs(iteratorResult);
    [[maybe_unused]] JSTaggedValue res = JSFunction::Call(info.Get());

    // 9. Perform ! AsyncGeneratorResumeNext(generator).
    JSAsyncGeneratorObject::AsyncGeneratorResumeNext(thread, generator);

    // 10. Return undefined.
    return globalConst->GetHandledUndefined();
}

// 27.6.3.5 AsyncGeneratorReject (generator, exception)
JSHandle<JSTaggedValue> JSAsyncGeneratorObject::AsyncGeneratorReject(JSThread *thread,
                                                                     const JSHandle<JSTaggedValue> &generator,
                                                                     const JSHandle<JSTaggedValue> &exception)
{
    [[maybe_unused]] EcmaHandleScope handleScope(thread);

    const GlobalEnvConstants *globalConst = thread->GlobalConstants();

    // 1. Assert: generator is an AsyncGenerator instance.
    ASSERT(generator->IsAsyncGeneratorObject());
    JSAsyncGeneratorObject *asyncGenObject = JSAsyncGeneratorObject::Cast(generator->GetHeapObject());

    // 2. Let queue be generator.[[AsyncGeneratorQueue]].
    JSHandle<TaggedQueue> queue(thread, TaggedQueue::Cast(asyncGenObject->GetAsyncGeneratorQueue().GetHeapObject()));

    // 3. Assert: queue is not an empty List.
    ASSERT(!queue->Empty());

    // 4. Let next be the first element of queue.
    // 5. Remove the first element from queue.
    JSTaggedValue next = queue->Pop(thread);
    JSHandle<TaggedArray> nextAsyncGeneratorRequest(thread, TaggedArray::Cast(next.GetHeapObject()));

    // 6. Let promiseCapability be next.[[Capability]].
    JSTaggedValue promiseCapabilityValue =
        nextAsyncGeneratorRequest->Get(thread, helpers::ToUnderlying(AsyncGeneratorRequest::PROMISE_CAPABILITY));
    JSHandle<PromiseCapability> promiseCapability(thread,
                                                  PromiseCapability::Cast(promiseCapabilityValue.GetHeapObject()));

    // 7. Perform ! Call(promiseCapability.[[Reject]], undefined, « exception »).
    JSHandle<JSTaggedValue> thisArg = globalConst->GetHandledUndefined();

    JSHandle<JSTaggedValue> reject(thread, promiseCapability->GetReject());
    auto info = NewRuntimeCallInfo(thread, reject, thisArg, JSTaggedValue::Undefined(), 1);
    info->SetCallArgs(exception);
    [[maybe_unused]] JSTaggedValue res = JSFunction::Call(info.Get());

    // 9. Perform ! AsyncGeneratorResumeNext(generator).
    JSAsyncGeneratorObject::AsyncGeneratorResumeNext(thread, generator);

    // 10. Return undefined.
    return globalConst->GetHandledUndefined();
}

// 27.6.3.6 AsyncGeneratorResumeNext ( generator )
JSHandle<JSTaggedValue> JSAsyncGeneratorObject::AsyncGeneratorResumeNext(JSThread *thread,
                                                                         const JSHandle<JSTaggedValue> &generator)
{
    [[maybe_unused]] EcmaHandleScope handleScope(thread);

    ObjectFactory *factory = thread->GetEcmaVM()->GetFactory();
    const GlobalEnvConstants *globalConst = thread->GlobalConstants();
    JSHandle<GlobalEnv> env = thread->GetEcmaVM()->GetGlobalEnv();

    // 1. Assert: generator is an AsyncGenerator instance.
    ASSERT(generator->IsAsyncGeneratorObject());
    JSHandle<JSAsyncGeneratorObject> asyncGenObject = JSHandle<JSAsyncGeneratorObject>::Cast(generator);

    // 2. Let state be generator.[[AsyncGeneratorState]].
    JSTaggedValue state = asyncGenObject->GetGeneratorState();

    // 3. Assert: state is not executing.
    ASSERT(!JSGeneratorObject::IsState(state, JSGeneratorState::EXECUTING));

    // 4. If state is awaiting-return, return undefined.
    if (JSGeneratorObject::IsState(state, JSGeneratorState::AWAITING_RETURN)) {
        return globalConst->GetHandledUndefined();
    }

    // 5. Let queue be generator.[[AsyncGeneratorQueue]].
    JSHandle<TaggedQueue> queue(thread, TaggedQueue::Cast(asyncGenObject->GetAsyncGeneratorQueue().GetHeapObject()));

    // 6. If queue is an empty List, return undefined.
    if (queue->Empty()) {
        return globalConst->GetHandledUndefined();
    }

    // 7. Let next be the value of the first element of queue.
    JSTaggedValue next = queue->Front();

    // 8. Assert: next is an AsyncGeneratorRequest record.
    JSHandle<TaggedArray> nextAsyncGeneratorRequest(thread, next);

    // 9. Let completion be next.[[Completion]].
    JSHandle<CompletionRecord> completion(
        thread, nextAsyncGeneratorRequest->Get(thread, helpers::ToUnderlying(AsyncGeneratorRequest::COMPLETION)));

    // 10. If completion is an abrupt completion, then
    if (completion->IsAbrupt()) {
        // a. If state is suspendedStart, then
        if (JSGeneratorObject::IsState(state, JSGeneratorState::SUSPENDED_START)) {
            // i. Set generator.[[AsyncGeneratorState]] to completed.
            asyncGenObject->SetState(thread, JSGeneratorState::COMPLETED);
            // ii. Set state to completed.
            state = JSTaggedValue(static_cast<int32_t>(JSGeneratorState::COMPLETED));
        }

        // b. If state is completed, then
        if (JSGeneratorObject::IsState(state, JSGeneratorState::COMPLETED)) {
            // i. If completion.[[Type]] is return, then
            if (completion->IsReturn()) {
                // 1. Set generator.[[AsyncGeneratorState]] to awaiting-return.
                asyncGenObject->SetState(thread, JSGeneratorState::AWAITING_RETURN);

                // 2. Let promise be ? PromiseResolve(%Promise%, completion.[[Value]]).
                JSHandle<JSPromise> promise(
                    thread, JSPromise::PromiseResolve(thread, env->GetPromiseFunction(),
                                                      JSHandle<JSTaggedValue>(thread, completion->GetValue())));
                RETURN_HANDLE_IF_ABRUPT_COMPLETION(JSTaggedValue, thread);

                // 3. Let stepsFulfilled be the algorithm steps defined in AsyncGeneratorResumeNext Return Processor
                // Fulfilled Functions.
                // 4. Let lengthFulfilled be the number of non-optional parameters of the function definition in
                // AsyncGeneratorResumeNext Return Processor Fulfilled Functions.
                // 5. Let onFulfilled be ! CreateBuiltinFunction(stepsFulfilled, lengthFulfilled, "", « [[Generator]]
                // »).
                JSHandle<JSAsyncGeneratorResolveNextFunction> onFulfilled =
                    factory->NewJSAsyncGeneratorResolveNextFunction(reinterpret_cast<void *>(
                        JSAsyncGeneratorResolveNextFunction::AsyncGeneratorResolveNextFulfilled));

                // 6. Set onFulfilled.[[Generator]] to generator.
                onFulfilled->SetAsyncGenerator(thread, asyncGenObject.GetTaggedValue());

                // 7. Let stepsRejected be the algorithm steps defined in AsyncGeneratorResumeNext Return Processor
                // Rejected Functions.
                // 8. Let lengthRejected be the number of non-optional parameters of the function definition in
                // AsyncGeneratorResumeNext Return Processor Rejected Functions.
                // 9. Let onRejected be ! CreateBuiltinFunction(stepsRejected, lengthRejected, "", « [[Generator]] »).
                JSHandle<JSAsyncGeneratorResolveNextFunction> onRejected =
                    factory->NewJSAsyncGeneratorResolveNextFunction(reinterpret_cast<void *>(
                        JSAsyncGeneratorResolveNextFunction::AsyncGeneratorResolveNextRejected));

                // 10. Set onRejected.[[Generator]] to generator.
                onRejected->SetAsyncGenerator(thread, asyncGenObject.GetTaggedValue());

                // 11. Perform ! PerformPromiseThen(promise, onFulfilled, onRejected).
                [[maybe_unused]] JSTaggedValue thenResult = builtins::promise::PerformPromiseThen(
                    thread, promise, JSHandle<JSTaggedValue>::Cast(onFulfilled),
                    JSHandle<JSTaggedValue>::Cast(onRejected), globalConst->GetHandledUndefined());

                // 12. Return undefined.
                return globalConst->GetHandledUndefined();
            }

            // ii. Else
            // 1. Assert: completion.[[Type]] is throw
            ASSERT(completion->IsThrow());

            // 2. Perform ! AsyncGeneratorReject(generator, completion.[[Value]]).
            [[maybe_unused]] JSHandle<JSTaggedValue> rejectResult =
                AsyncGeneratorReject(thread, generator, JSHandle<JSTaggedValue>(thread, completion->GetValue()));

            // 3. Return undefined.
            return globalConst->GetHandledUndefined();
        }
    } else if (JSGeneratorObject::IsState(state, JSGeneratorState::COMPLETED)) {
        // 11. Else if state is completed, return ! AsyncGeneratorResolve(generator, undefined, true).
        return AsyncGeneratorResolve(thread, generator, globalConst->GetHandledUndefined(), true);
    }

    // 12. Assert: state is either suspendedStart or suspendedYield.
    ASSERT(JSGeneratorObject::IsState(state, JSGeneratorState::SUSPENDED_START) ||
           JSGeneratorObject::IsState(state, JSGeneratorState::SUSPENDED_YIELD));

    // 13. Let genContext be generator.[[AsyncGeneratorContext]].
    JSHandle<GeneratorContext> genContext(thread, asyncGenObject->GetGeneratorContext());

    // 16. Set generator.[[AsyncGeneratorState]] to executing
    asyncGenObject->SetState(thread, JSGeneratorState::EXECUTING);

    // 14. Let callerContext be the running execution context.
    // 15. Suspend callerContext.
    // 17. Push genContext onto the execution context stack; genContext is now the running execution context. */
    // 18. Resume the suspended evaluation of genContext using completion as the result of the operation that suspended
    // it. Let result be the completion record returned by the resumed computation.

    if (completion->IsReturn()) {
        [[maybe_unused]] JSHandle<JSTaggedValue> result =
            GeneratorHelper::Continue(thread, genContext, GeneratorResumeMode::RETURN, completion->GetValue());
    } else if (completion->IsThrow()) {
        [[maybe_unused]] JSHandle<JSTaggedValue> result =
            GeneratorHelper::Continue(thread, genContext, GeneratorResumeMode::THROW, completion->GetValue());
    } else {
        [[maybe_unused]] JSHandle<JSTaggedValue> result =
            GeneratorHelper::Continue(thread, genContext, GeneratorResumeMode::NEXT, completion->GetValue());
    }
    // 19. Assert: result is never an abrupt completion.
    // 20. Assert: When we return here, genContext has already been removed from the execution context stack and
    // callerContext is the currently running execution context.

    // 21. Return undefined.
    return globalConst->GetHandledUndefined();
}

// 27.6.3.7 AsyncGeneratorEnqueue(generator, value)
JSHandle<JSTaggedValue> JSAsyncGeneratorObject::AsyncGeneratorEnqueue(JSThread *thread,
                                                                      const JSHandle<JSTaggedValue> &generator,
                                                                      const JSHandle<CompletionRecord> &completion)
{
    [[maybe_unused]] EcmaHandleScope handleScope(thread);

    ObjectFactory *factory = thread->GetEcmaVM()->GetFactory();
    JSHandle<GlobalEnv> env = thread->GetEcmaVM()->GetGlobalEnv();

    // 1. Let promiseCapability be ! NewPromiseCapability(%Promise%).
    JSHandle<PromiseCapability> promiseCapability =
        JSPromise::NewPromiseCapability(thread, JSHandle<JSTaggedValue>::Cast(env->GetPromiseFunction()));

    // 2. Let check be AsyncGeneratorValidate(generator, generatorBrand).
    [[maybe_unused]] JSTaggedValue check = AsyncGeneratorValidate(thread, generator);

    // 3. If check is an abrupt completion, then
    if (UNLIKELY(thread->HasPendingException())) {
        // a. Let badGeneratorError be a newly created TypeError object.
        JSTaggedValue badGeneratorError = thread->GetException();
        thread->ClearException();

        // b. Perform ! Call(promiseCapability.[[Reject]], undefined, « badGeneratorError »).
        const GlobalEnvConstants *globalConst = thread->GlobalConstants();
        JSHandle<JSTaggedValue> thisArg = globalConst->GetHandledUndefined();
        JSHandle<JSTaggedValue> reject(thread, promiseCapability->GetReject());
        auto info = NewRuntimeCallInfo(thread, reject, thisArg, JSTaggedValue::Undefined(), 1);
        info->SetCallArgs(badGeneratorError);
        [[maybe_unused]] JSTaggedValue res = JSFunction::Call(info.Get());
        // c. Return promiseCapability.[[Promise]].
        return JSHandle<JSTaggedValue>(thread, promiseCapability->GetPromise());
    }

    JSHandle<JSAsyncGeneratorObject> asyncGenObject(thread, JSAsyncGeneratorObject::Cast(generator->GetHeapObject()));

    // 4. Let queue be generator.[[AsyncGeneratorQueue]].
    JSHandle<TaggedQueue> queue(thread, TaggedQueue::Cast(asyncGenObject->GetAsyncGeneratorQueue().GetHeapObject()));

    // 5. Let request be AsyncGeneratorRequest { [[Completion]]: completion, [[Capability]]: promiseCapability }.
    JSHandle<TaggedArray> request = factory->NewTaggedArray(2);
    request->Set(thread, helpers::ToUnderlying(AsyncGeneratorRequest::COMPLETION), completion.GetTaggedValue());
    request->Set(thread, helpers::ToUnderlying(AsyncGeneratorRequest::PROMISE_CAPABILITY),
                 promiseCapability.GetTaggedValue());
    JSHandle<JSTaggedValue> requestValue(thread, request.GetTaggedValue());

    // 6. Append request to the end of queue.
    JSTaggedValue newQueue(TaggedQueue::Push(thread, queue, requestValue));
    asyncGenObject->SetAsyncGeneratorQueue(thread, newQueue);

    // 7. Let state be generator.[[AsyncGeneratorState]].
    // 8. If state is not executing, then
    if (!asyncGenObject->IsExecuting()) {
        AsyncGeneratorResumeNext(thread, generator);
    }

    // 9. Return promiseCapability.[[Promise]].
    return JSHandle<JSTaggedValue>(thread, promiseCapability->GetPromise());
}
}  // namespace ark::ecmascript
